package com.fa4j.code.mate.convert;

import com.fa4j.code.mate.model.*;
import com.fa4j.common.base.convert.ConverterConfig;
import org.mapstruct.*;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.*;

@Mapper(config = ConverterConfig.class)
public interface CodeMateConverter {

    @Mapping(target = "type", ignore = true)
    @SubclassMapping(source = CodeMateProject.class, target = CodeMate.class)
    @SubclassMapping(source = CodeMateModule.class, target = CodeMate.class)
    @SubclassMapping(source = CodeMateModel.class, target = CodeMate.class)
    @SubclassMapping(source = CodeMateField.class, target = CodeMate.class)
    @SubclassMapping(source = CodeMateMethod.class, target = CodeMate.class)
    CodeMate toEntity(AbstractCodeMate obj);

    void updateObj(CodeMate entity, @MappingTarget AbstractCodeMate obj);

    @AfterMapping
    default void afterToObj(CodeMate entity, @MappingTarget AbstractCodeMate obj) {
        Map<String, Object> map = entity.getData();
        if (entity.getData() != null) {
            List<Field> fieldList = getDiffFieldList(obj.getClass());
            for (Field field : fieldList) {
                field.setAccessible(true);
                Object value = map.get(field.getName());
                if (value != null) {
                    if (field.getType().isEnum()) {
                        for (Object enumConstant : field.getType().getEnumConstants()) {
                            if (Objects.equals(value, ((Enum<?>) enumConstant).name())) {
                                value = enumConstant;
                                break;
                            }
                        }
                    }
                    ReflectionUtils.setField(field, obj, value);
                }
            }
        }
    }

    default List<Field> getDiffFieldList(Class<? extends AbstractCodeMate> clazz) {
        List<Field> fieldList = new ArrayList<>();
        ReflectionUtils.doWithFields(clazz, field -> {
            Field mateField = ReflectionUtils.findField(AbstractCodeMate.class, field.getName());
            if (mateField == null || mateField.getType() != field.getType()) {
                fieldList.add(field);
            }
        });
        return fieldList;
    }

    @AfterMapping
    default void afterProjectTo(@MappingTarget CodeMate entity, AbstractCodeMate obj) {
        List<Field> fieldList = getDiffFieldList(obj.getClass());
        Map<String, Object> map = new HashMap<>();
        for (Field field : fieldList) {
            field.setAccessible(true);
            Object value = ReflectionUtils.getField(field, obj);
            if (value != null) {
                map.put(field.getName(), value);
            }
        }
        entity.setData(map);
        if (obj instanceof CodeMateProject project) {
            entity.setType(CodeMateType.PROJECT);
            entity.setName(project.getProjectName());
            entity.setPriority(project.getModuleCode());
            entity.setParentId(0);
        }
        if (obj instanceof CodeMateModule module) {
            entity.setType(CodeMateType.MODULE);
            entity.setName(module.getModuleName());
            entity.setPriority(module.getModuleCode());
        }
        if (obj instanceof CodeMateModel) {
            entity.setType(CodeMateType.MODEL);
        }
        if (obj instanceof CodeMateField) {
            entity.setType(CodeMateType.FIELD);
        }
        if (obj instanceof CodeMateMethod) {
            entity.setType(CodeMateType.METHOD);
        }
    }


    /// project

    CodeMateProject toProject(CodeMateProjectCreateCmd cmd);

    void updateProject(CodeMateProjectModifyCmd cmd, @MappingTarget CodeMateProject project);

    CodeMateProject toProject(CodeMate entity);

    /// module

    CodeMateModule toModule(CodeMateModuleCreateCmd cmd);

    void updateModule(CodeMateModuleModifyCmd cmd, @MappingTarget CodeMateModule module);

    CodeMateModule toModule(CodeMate entity);

    /// model

    CodeMateModel toModel(CodeMateModelCreateCmd cmd);

    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    void updateModel(CodeMateModelModifyCmd cmd, @MappingTarget CodeMateModel model);

    @Mapping(target = "type", ignore = true)
    CodeMateModel toModel(CodeMate entity);


    /// field

    CodeMateField toField(CodeMateFieldCreateCmd cmd);

    void updateField(CodeMateFieldModifyCmd cmd, @MappingTarget CodeMateField field);

    @Mapping(target = "type", ignore = true)
    CodeMateField toField(CodeMate entity);


    /// method

    CodeMateMethod toMethod(CodeMateMethodCreateCmd cmd);

    void updateMethod(CodeMateMethodModifyCmd cmd, @MappingTarget CodeMateMethod method);

    @Mapping(target = "type", ignore = true)
    CodeMateMethod toMethod(CodeMate entity);


    @Mapping(target = "type", constant = "PROJECT")
    CodeMateQuery toMateQuery(CodeMateProjectQuery query);
}
